package com.crazy.dao.sys.impl;

import static com.crazy.common.page.Pager.DEFAULT_COUNT;
import static org.hibernate.criterion.DetachedCriteria.forClass;
import static org.hibernate.criterion.Example.create;
import static org.hibernate.criterion.MatchMode.ANYWHERE;
import static org.hibernate.criterion.Projections.rowCount;
import static org.hibernate.criterion.Restrictions.eq;
import static org.hibernate.criterion.Restrictions.in;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.Id;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.Order;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.NoRepositoryBean;

import com.crazy.common.page.Pager;
import com.crazy.dao.sys.BaseDao;
import com.crazy.support.QueryHelper;

/**
 * DAO基础类——实现类
 * 
 * @author Zhang Weiwei
 * @since 2013-9-8下午4:59:19
 */
@SuppressWarnings("unchecked")
@NoRepositoryBean
public class MyBaseDao<Entity> extends
		SimpleJpaRepository<Entity, Serializable> implements BaseDao<Entity> {

	private Class<Entity> clazz;
	private EntityManager em;

	public MyBaseDao(Class<Entity> domainClass, EntityManager entityManager) {
		super(domainClass, entityManager);
		this.clazz = domainClass;
		this.em = entityManager;
	}

	@Override
	public void delete(Entity entity) {
		this.getSession().delete(entity);
	}

	@Override
	public <PK> void deleteByPK(PK pk) {
		try {
			Entity entity = this.clazz.newInstance();
			Field[] fields = this.clazz.getDeclaredFields();
			Field field = null;
			for (Field f : fields) {
				if (f.getAnnotation(Id.class) != null) {
					field = f;
					break;
				}
			}
			if (field == null) {
				Method[] methods = this.clazz.getMethods();
				Method method = null;
				for (Method m : methods) {
					if (m.getAnnotation(Id.class) != null) {
						method = m;
						break;
					}
				}
				String name = method.getName().replaceFirst("g", "s");
				for (Method m : methods) {
					if (m.getName().equals(name)) {
						method = m;
						break;
					}
				}
				method.invoke(entity, pk);
			} else {
				field.setAccessible(true);
				field.set(entity, pk);
			}
			this.getSession().delete(entity);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public Page<Entity> findAll(Integer pageNum, Integer pageSize,
			Order... orders) {
		return this.findByCriteria(pageNum, pageSize, this.getCriteria(),
				orders);
	}

	@Override
	public List<Entity> findAll(Integer maxRow, Order... orders) {
		Criteria c = this.getCriteria();
		c = c.setMaxResults(maxRow);
		this.setOrder(c, orders);
		List<Entity> list = c.list();
		return list;
	}

	@Override
	public List<Entity> findAll(Order... orders) {
		Criteria c = this.getCriteria();
		this.setOrder(c, orders);
		return c.list();
	}

	@Override
	public List<Entity> findByDetachedCriteria(DetachedCriteria dc,
			Integer maxRow, Order... orders) {
		Criteria c = dc.getExecutableCriteria(this.getSession());
		this.setOrder(dc, orders);
		c.setMaxResults(maxRow);
		return c.list();
	}

	@Override
	public <T> List<T> findByDetachedCriteria(DetachedCriteria dc,
			Order... orders) {
		Criteria c = dc.getExecutableCriteria(this.getSession());
		this.setOrder(dc, orders);
		return c.list();
	}

	@Override
	public Page<Entity> findByDetachedCriteria(Integer pageNum,
			Integer pageSize, DetachedCriteria dc, Order... orders) {
		Criteria c = dc.getExecutableCriteria(this.getSession());
		this.setOrder(dc, orders);
		return this.findByCriteria(pageNum, pageSize, c);
	}

	@Override
	public List<Entity> findByExample(Entity entity, Boolean excludeZeroes,
			Boolean excludeNone, Boolean enableLike, Boolean ignoreCase,
			Order... orders) {
		Criteria c = this.getCriteria();
		Example example = this.getExample(entity, excludeZeroes, excludeNone,
				enableLike, ignoreCase);
		c.add(example);
		this.setOrder(c, orders);
		return c.list();
	}

	@Override
	public List<Entity> findByExample(Entity entity, Order... orders) {
		Criteria c = this.getCriteria();
		c.add(create(entity));
		this.setOrder(c, orders);
		return c.list();
	}

	@Override
	public List<Entity> findByProperty(String propertyName, Object value,
			Order... orders) {
		Criteria c = this.getCriteria();
		c.add(eq(propertyName, value));
		this.setOrder(c, orders);
		return c.list();
	}

	@Override
	public Entity findUniqueResult(DetachedCriteria dc) {
		Criteria c = dc.getExecutableCriteria(this.getSession());
		return (Entity) this.uniqueResult(c.list());
	}

	@Override
	public Entity findUniqueResult(String propertyName, Object value) {
		Criteria c = this.getCriteria();
		c.add(eq(propertyName, value));
		return (Entity) this.uniqueResult(c.list());
	}

	@Override
	public List<Entity> findUseInOfArray(String propertyName, Object... values) {
		Criteria c = this.getCriteria();
		c.add(in(propertyName, values));
		return c.list();
	}

	@Override
	public Entity get(Serializable pk) {
		return (Entity) this.getSession().get(this.clazz, pk);
	}

	@Override
	public DetachedCriteria getDetachedCriteria() {
		DetachedCriteria dc = forClass(this.clazz);
		return dc;
	}

	@Override
	public Entity load(Serializable pk) {
		return (Entity) this.getSession().load(this.clazz, pk);
	}

	@Override
	public <S extends Entity> S save(S entity) {
		this.getSession().save(entity);
		return entity;
	}

	@Override
	public Entity saveOrUpdate(Entity entity) {
		this.getSession().saveOrUpdate(entity);
		return entity;
	}

	@Override
	public Entity update(Entity entity) {
		this.getSession().update(entity);
		return entity;
	}

	@Override
	public List<Entity> findByQuery(String hql, String[] names, Object[] values) {
		Query query = this.getQuery(hql);
		this.setParameters(query, names, values);
		return query.list();
	}

	@Override
	public List<Object[]> findBySQLQuery(String sql, String[] names,
			Object[] values) {
		SQLQuery query = this.getSQLQuery(sql);
		this.setParameters(query, names, values);
		return query.list();
	}

	@Override
	public List<Entity> findBySQLQueryToEntity(String sql, String[] names,
			Object[] values) {
		SQLQuery query = this.getSQLQuery(sql).addEntity(this.clazz);
		this.setParameters(query, names, values);
		return query.list();
	}

	@Override
	public Page<Entity> findByQuery(String hql, String[] names,
			Object[] values, Integer pageNum, Integer pageSize) {
		pageNum = this.getPageNum(pageNum);
		pageSize = this.getPageSize(pageSize);
		List<Entity> list = new ArrayList<>();
		QueryHelper helper = new QueryHelper(hql);
		Query query = this.getQuery(helper.getCountQuery());
		this.setParameters(query, names, values);
		Number rowCount = (Number) this.uniqueResult(query.list());
		if (rowCount.intValue() > 0) {
			int first = (pageNum - 1) * pageSize;
			query = this.getQuery(hql);
			this.setParameters(query, names, values);
			list = query.setFirstResult(first).setMaxResults(pageSize).list();
		}
		return this.getPage(list, pageNum, pageSize, rowCount);
	}

	@Override
	public Page<Object[]> findBySQLQuery(String sql, String[] names,
			Object[] values, Integer pageNum, Integer pageSize) {
		List<Object[]> list = new ArrayList<>();
		pageNum = this.getPageNum(pageNum);
		pageSize = this.getPageSize(pageSize);
		QueryHelper helper = new QueryHelper(sql);
		SQLQuery query = this.getSQLQuery(helper.getCountQuery());
		this.setParameters(query, names, values);
		Number rowCount = (Number) this.uniqueResult(query.list());
		if (rowCount.intValue() > 0) {
			int first = (pageNum - 1) * pageSize;
			query = this.getSQLQuery(sql);
			this.setParameters(query, names, values);
			list = query.setFirstResult(first).setMaxResults(pageSize).list();
		}
		return this.getPage(list, pageNum, pageSize, rowCount);
	}

	@Override
	public Page<Entity> findBySQLQueryToEntity(String sql, String[] names,
			Object[] values, Integer pageNum, Integer pageSize) {
		List<Entity> list = new ArrayList<>();
		pageNum = this.getPageNum(pageNum);
		pageSize = this.getPageSize(pageSize);
		QueryHelper helper = new QueryHelper(sql);
		SQLQuery query = this.getSQLQuery(helper.getCountQuery());
		this.setParameters(query, names, values);
		Number rowCount = (Number) this.uniqueResult(query.list());
		if (rowCount.intValue() > 0) {
			int first = (pageNum - 1) * pageSize;
			query = this.getSQLQuery(sql).addEntity(this.clazz);
			this.setParameters(query, names, values);
			list = query.setFirstResult(first).setMaxResults(pageSize).list();
		}
		return this.getPage(list, pageNum, pageSize, rowCount);
	}

	@Override
	public <T> List<T> findByQuery(String hql, Map<String, Object> params) {
		params = this.getParams(params);
		return this.getQuery(hql).setProperties(params).list();
	}

	@Override
	public Page<Entity> findByQuery(String hql, Map<String, Object> params,
			Integer pageNum, Integer pageSize) {
		params = this.getParams(params);
		pageNum = this.getPageNum(pageNum);
		pageSize = this.getPageSize(pageSize);
		List<Entity> list = new ArrayList<>();
		QueryHelper helper = new QueryHelper(hql);
		Number rowCount = (Number) this.uniqueResult(this
				.getQuery(helper.getCountQuery()).setProperties(params).list());
		if (rowCount.intValue() > 0) {
			int first = (pageNum - 1) * pageSize;
			list = this.getQuery(hql).setProperties(params)
					.setFirstResult(first).setMaxResults(pageSize).list();
		}
		return this.getPage(list, pageNum, pageSize, rowCount);
	}

	@Override
	public List<Object[]> findBySQLQuery(String sql, Map<String, Object> params) {
		params = this.getParams(params);
		return this.getSQLQuery(sql).setProperties(params).list();
	}

	@Override
	public Page<Object[]> findBySQLQuery(String sql,
			Map<String, Object> params, Integer pageNum, Integer pageSize) {
		params = this.getParams(params);
		List<Object[]> list = new ArrayList<>();
		pageNum = this.getPageNum(pageNum);
		pageSize = this.getPageSize(pageSize);
		QueryHelper helper = new QueryHelper(sql);
		Query query = this.getSQLQuery(helper.getCountQuery()).setProperties(
				params);
		Number rowCount = (Number) this.uniqueResult(query.list());
		if (rowCount.intValue() > 0) {
			int first = (pageNum - 1) * pageSize;
			list = this.getSQLQuery(sql).setProperties(params)
					.setFirstResult(first).setMaxResults(pageSize).list();
		}
		return this.getPage(list, pageNum, pageSize, rowCount);
	}

	@Override
	public List<Entity> findBySQLQueryToEntity(String sql,
			Map<String, Object> params) {
		params = this.getParams(params);
		return this.getSQLQuery(sql).addEntity(this.clazz)
				.setProperties(params).list();
	}

	@Override
	public Page<Entity> findBySQLQueryToEntity(String sql,
			Map<String, Object> params, Integer pageNum, Integer pageSize) {
		params = this.getParams(params);
		List<Entity> list = new ArrayList<>();
		pageNum = this.getPageNum(pageNum);
		pageSize = this.getPageSize(pageSize);
		QueryHelper helper = new QueryHelper(sql);
		Query query = this.getSQLQuery(helper.getCountQuery()).setProperties(
				params);
		Number rowCount = (Number) this.uniqueResult(query.list());
		if (rowCount.intValue() > 0) {
			int first = (pageNum - 1) * pageSize;
			list = this.getSQLQuery(sql).addEntity(this.clazz)
					.setProperties(params).setFirstResult(first)
					.setMaxResults(pageSize).list();
		}
		return this.getPage(list, pageNum, pageSize, rowCount);
	}

	protected Session getSession() {
		return (Session) em.getDelegate();
	}

	protected Criteria getCriteria() {
		return this.getSession().createCriteria(this.clazz);
	}

	protected Page<Entity> findByCriteria(Integer pageNum, Integer pageSize,
			Criteria c, Order... orders) {
		pageNum = this.getPageNum(pageNum);
		pageSize = this.getPageSize(pageSize);
		List<Entity> list = new ArrayList<>();
		Number rowCount = (Number) this.uniqueResult(c
				.setProjection(rowCount()).list());
		if (rowCount.intValue() > 0) {
			int first = (pageNum - 1) * pageSize;
			c = c.setProjection(null).setFirstResult(first)
					.setMaxResults(pageSize);
			this.setOrder(c, orders);
			list = c.list();
		}
		return this.getPage(list, pageNum, pageSize, rowCount);
	}

	private <T> Page<T> getPage(List<T> list, Integer pageNum,
			Integer pageSize, Number rowCount) {
		Pageable pageable = new PageRequest(pageNum, pageSize);
		Page<T> pager = new Pager<T>(list, pageable, rowCount);
		return pager;
	}

	private int getPageSize(Integer pageSize) {
		return (pageSize == null || pageSize < 1) ? DEFAULT_COUNT : pageSize;
	}

	private int getPageNum(Integer pageNum) {
		return (pageNum == null || pageNum < 1) ? 1 : pageNum;
	}

	private Map<String, Object> getParams(Map<String, Object> params) {
		return params == null ? new HashMap<String, Object>() : params;
	}

	private Example getExample(Entity entity, Boolean excludeZeroes,
			Boolean excludeNone, Boolean enableLike, Boolean ignoreCase) {
		Example example = create(entity);
		if (excludeZeroes != null && excludeZeroes) {
			example.excludeZeroes();
		}
		if (excludeNone != null && excludeNone) {
			example.excludeNone();
		}
		if (enableLike != null && enableLike) {
			example.enableLike(ANYWHERE);
		}
		if (ignoreCase != null && ignoreCase) {
			example.ignoreCase();
		}
		return example;
	}

	private Query getQuery(String hql) {
		return this.getSession().createQuery(hql);
	}

	private SQLQuery getSQLQuery(String sql) {
		return this.getSession().createSQLQuery(sql);
	}

	private void setOrder(Criteria c, Order... orders) {
		for (Order order : orders) {
			c.addOrder(order);
		}
	}

	private void setOrder(DetachedCriteria dc, Order... orders) {
		for (Order order : orders) {
			dc.addOrder(order);
		}
	}

	private void setParameters(Query query, String[] names, Object[] values) {
		if (names != null && values != null && names.length == values.length) {
			for (int i = 0; i < names.length; i++) {
				String name = names[i];
				Object value = values[i];
				this.setParameter(query, name, value);
			}
		}
	}

	private void setParameter(Query query, String name, Object value) {
		if (value instanceof Collection) {
			query.setParameterList(name, (Collection<?>) value);
		} else if (value instanceof Object[]) {
			query.setParameterList(name, (Object[]) value);
		} else {
			query.setParameter(name, value);
		}
	}

	private Object uniqueResult(List<?> list) {
		return list.isEmpty() ? null : list.get(0);
	}
}
